4

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

在各个子类中实现__init__()

当我们看到创建Card对象的工厂函数,再看看Card类设计。我想我们可能要重构牌值转换功能,因为这是Card类自身应该负责的内容。这会将初始化向下延伸到每个子类。

这需要共用的超类初始化以及特定的子类初始化。我们要谨遵Don't Repeat Yourself(DRY)原则来保持代码可以被克隆到每一个子类中。

下面的示例展示了每个子类初始化的职责:

pythonclass Card:
    pass

class NumberCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = str(rank)
        self.hard = self.soft = rank

class AceCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = "A"
        self.hard, self.soft =  1, 11

class FaceCard(Card):
    def  __init__(self, rank, suit):
        self.suit = suit
        self.rank = {11: 'J', 12: 'Q', 13: 'K'}[rank]
        self.hard = self.soft = 10

这仍是清晰的多态。然而,缺乏一个真正的共用初始化,会导致一些冗余。缺点在于重复初始化suit,所以必须将其抽象到超类中。各子类的__init__()会对超类的__init__()做显式的引用。

该版本的Card类有一个超类级别的初始化函数用于各子类,如下面代码片段所示:

pythonclass Card:
    def __init__(self, rank, suit, hard, soft):
        self.rank = rank
        self.suit = suit
        self.hard = hard
        self.soft = soft

class NumberCard(Card):
    def  __init__(self, rank, suit):
        super().__init__(str(rank), suit, rank, rank)

class AceCard(Card):
    def  __init__(self, rank, suit):
        super().__init__("A", suit, 1, 11)

class FaceCard(Card):
    def  __init__(self, rank, suit):
        super().__init__({11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10)

我们在子类和父类都提供了__init__()函数。好处是简化了我们的工厂函数,如下面代码片段所示:

pythondef card10(rank, suit):
    if rank == 1: 
        return AceCard(rank, suit)
    elif 2 <= rank < 11: 
        return NumberCard(rank, suit)
    elif 11 <= rank < 14: 
        return FaceCard(rank, suit)
    else:
       raise Exception("Rank out of range")

简化工厂函数不应该是我们关注的焦点。不过我们从这可以看到一些变化,我们创建了比较复杂的__init__()函数,而对工厂函数却有一些较小的改进。这是比较常见的权衡。

工厂函数封装复杂性

在复杂的__init__()方法和工厂函数之间有个权衡。最好就是坚持更直接,更少程序员友好的__init__()方法,并将复杂性推给工厂函数。如果你想封装复杂结构,工厂函数可以做的很好。

简单复合对象

复合对象也可被称为容器。我们来看一个简单的复合对象:一副单独的牌。这是一个基本的集合。事实上它是如此基本,以至于我们不用过多的花费心思,直接使用简单的list做为一副牌。

在设计一个新类之前,我们需要问这个问题:使用一个简单的list是否合适?

我们可以使用random.shuffle()来洗牌和使用deck.pop()发牌到玩家手里。

一些程序员急于定义新类就像使用内置类一样草率,这很容易违反面向对象的设计原则。我们要避免一个新类像如下代码片段所示:

pythond = [card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
random.shuffle(d)
hand = [d.pop(), d.pop()]

如果就这么简单,为什么要写一个新类?

答案并不完全清楚。一个好处是,提供一个简化的、未实现接口的对象。正如我们前面提到的工厂函数一样,但在Python中类并不是一个硬性要求。

在前面的代码中,一副牌只有两个简单的用例和一个似乎并不够简化的类定义。它的优势在于隐藏实现的细节,但细节是如此微不足道,揭露它们几乎没有任何意义。在本章中,我们的关注主要放在__init__()方法上,我们将看一些创建并初始化集合的设计。

设计一个对象集合,有以下三个总体设计策略:

  • 封装:该设计模式是现有的集合的定义。这可能是Facade设计模式的一个例子。

  • 继承:该设计模式是现有的集合类,是普通子类的定义。

  • 多态:从头开始设计。我们将在第六章看看《创建容器和集合》。

这三个概念是面向对象设计的核心。在设计一个类的时候我们必须总是这样做选择。

1. 封装集合类

以下是封装设计,其中包含一个内部集合:

pythonclass Deck:
    def __init__(self):
        self._cards = [card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
        random.shuffle(self._cards)

    def pop(self):
        return self._cards.pop()

我们已经定义了Deck,内部集合是一个list对象。Deckpop()方法简单的委托给封装好的list对象。

然后我们可以通过下面这样的代码创建一个Hand实例:

pythond = Deck()
hand = [d.pop(), d.pop()]

一般来说,Facade设计模式或封装好方法的类是简单的被委托给底层实现类的。这个委托会变得冗长。对于一个复杂的集合,我们可以委托大量方法给封装的对象。

2. 继承集合类

封装的另一种方法是继承内置类。这样做的优势是没有重新实现pop()方法,因为我们可以简单地继承它。

pop()的优点就是不用写过多的代码就能创建类。在这个例子中,继承list类的缺点是提供了一些我们不需要的函数。

下面是继承内置listDeck定义:

pythonclass Deck2(list):
    def __init__(self):
        super().__init__(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade))
        random.shuffle(self)

在某些情况下,为了拥有合适的类行为,我们的方法将必须显式地使用超类。在下面的章节中我们将会看到其他相关示例。

我们利用超类的__init__()方法填充我们的list对象来初始化单副扑克牌,然后我们洗牌。pop()方法只是简单从list继承过来且工作完美。从list继承的其他方法也能一起工作。

3. 更多的需求和另一种设计

在赌场中,牌通常从牌盒发出,里面有半打喜忧参半的扑克牌。这个原因使得我们有必要建立自己版本的Deck,而不是简单、纯粹的使用list对象。

此外,牌盒里的牌并不完全发完。相反,会插入标记牌。因为有标记牌,有些牌会被保留,而不是用来玩。

下面是包含多组52张牌的Deck定义:

pythonclass Deck3(list):
    def __init__(self, decks=1):
        super().__init__()
        for i in range(decks):
            self.extend(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade))
        random.shuffle(self)
        burn = random.randint(1, 52)
        for i in range(burn): 
            self.pop()

在这里,我们使用super().__init__()来构建一个空集合。然后,我们使用self.extend()添加多次52张牌。由于我们在这个类中没有使用覆写,所以我们可以使用super().extend()

我们还可以通过super().__init__(),使用更深层嵌套的生成器表达式执行整个任务。如下面代码片段所示:

python(card6(r+1, s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks))

这个类为我们提供了一个Card实例的集合,我们可以使用它来模仿赌场21点发牌的盒子。

在赌场有一个奇怪的仪式,他们会翻开废弃的牌。如果我们要设计一个记牌玩家策略,我们可能需要效仿这种细微差别。

复杂复合对象

以下是21点Hand类描述的一个例子,很适合模拟玩家策略:

pythonclass Hand:
    def __init__(self, dealer_card):
        self.dealer_card = dealer_card
        self.cards = []
    def hard_total(self):
        return sum(c.hard for c in self.cards)
    def soft_total(self):
        return sum(c.soft for c in self.cards)

在这个例子中,我们有一个基于__init__()方法参数的self.dealer_card实例变量。self.cards实例变量是不基于任何参数的。这个初始化创建了一个空集合。

我们可以使用下面的代码去创建一个Hand实例

pythond = Deck()
h = Hand(d.pop())
h.cards.append(d.pop())
h.cards.append(d.pop())

缺点就是有一个冗长的语句序列被用来构建一个Hand的实例对象。它难以序列化Hand对象并像这样初始化来重建。尽管我们在这个类中创建一个显式的append()方法,它仍将采取多个步骤来初始化集合。

我们可以尝试创建一个接口,但这并不是一件简单的事情,对于Hand对象它只是在语法上发生了变化。接口仍然会导致多种方法计算。当我们看到第2部分中的《序列化和持久化》,我们倾向于使用接口,一个类级别的函数,理想情况下,应该是类的构造函数。我们将在第9章的《序列化和存储——JSON、YAML、Pickle、CSV和XML》深入研究。

还要注意一些不完全遵循21点规则的方法功能。在第二章《通过Python无缝地集成——基本的特殊方法》中我们会回到这个问题。

1. 复杂复合对象初始化

理想情况下,__init__()方法会创建一个对象的完整实例。这是一个更复杂的容器,当你在创建一个包含内部其他对象集合的完整实例的时候。如果我们可以一步就能构建这个复合对象,它将是非常有帮助的。

逐步增加项目的方法和一步加载所有项目的方法是一样的。

例如,我们可能有如下面的代码片段所示的类:

pythonclass Hand2:
   def __init__(self, dealer_card, *cards):
       self.dealer_card = dealer_card
       self.cards = list(cards)
   def hard_total(self):
       return sum(c.hard for c in self.cards)
   def soft_total(self):
       return sum(c.soft for c in self.cards)

这个初始化一步就设置了所有实例变量。另一个方法就是之前那样的类定义。我们可以有两种方式构建一个Hand2对象。第一个示例一次加载一张牌到Hand2对象:

pythond = Deck()
P = Hand2(d.pop())
p.cards.append(d.pop())
p.cards.append(d.pop())

第二个示例使用*cards参数一步加载一序列的Card类:

pythond = Deck()
h = Hand2(d.pop(), d.pop(), d.pop())

对于单元测试,在一个声明中使用这种方式通常有助于构建复合对象。更重要的是,这种简单、一步的计算来构建复合对象有利于下一部分的序列化技术。


wanyoung
2k 声望363 粉丝